Inside Macintosh: Memory

Previous | Chapter Top | Chapter Contents | Next

Dangling Pointers

Accessing a relocatable block by double indirection, through its handle instead of through its master pointer, requires an extra memory reference. For efficiency, you might sometimes want to dereference the handle--that is, make a copy of the block's master pointer--and then use that pointer to access the block by single indirection. When you do this, however, you need to be particularly careful. Any operation that allocates space from the heap might cause the relocatable block to be moved or purged. In that event, the block's master pointer is correctly updated, but your copy of the master pointer is not. As a result, your copy of the master pointer is a dangling pointer.

Dangling pointers are likely to make your application crash or produce garbled output. Unfortunately, it is often easy during debugging to overlook situations that could leave pointers dangling, because pointers dangle only if the relocatable blocks that they reference actually move. Routines that can move or purge memory do not necessarily do so unless memory space is tight. Thus, if you improperly dereference a handle in a section of code, that code might still work properly most of the time. If, however, a dangling pointer does cause errors, they can be very difficult to trace.

This section describes a number of situations that can cause dangling pointers and suggests some ways to avoid them.

Compiler Dereferencing

Some of the most difficult dangling pointers to isolate are not caused by any explicit dereferencing on your part, but by implicit dereferencing on the part of the compiler. For example, suppose you use a handle called myHandle to access the fields of a record in a relocatable block. You might use Pascal's WITH statement to do so, as follows:

WITH myHandle^^ DO
    BEGIN
        ...
    END;

A compiler is likely to dereference myHandle so that it can access the fields of the record without double indirection. However, if the code between the BEGIN and END statements causes the Memory Manager to move or purge memory, you are likely to end up with a dangling pointer.

The easiest way to prevent dangling pointers is simply to lock the relocatable block whose data you want to read or write. Because the block is locked and cannot move, the master pointer is guaranteed always to point to the beginning of the block's data. Listing 1-1 illustrates one way to avoid dangling pointers by locking a relocatable block.

Listing 1 Locking a block to avoid dangling pointers

VAR
    origState:      SignedByte;         {original attributes of handle}

origState := HGetState(Handle(myData));             {get handle attributes}
MoveHHi(Handle(myData));                            {move the handle high}
HLock(Handle(myData));                              {lock the handle}
WITH myData^^ DO                                    {fill in window data}
    BEGIN
        editRec := TENew(gDestRect, gViewRect);
        vScroll := GetNewControl(rVScroll, myWindow);
        hScroll := GetNewControl(rHScroll, myWindow);
        fileRefNum := 0;
        windowDirty := FALSE;
    END;
HSetState(origState);                               {reset handle attributes}

The handle myData needs to be locked before the WITH statement because the functions TENew and GetNewControl allocate memory and hence might move the block whose handle is myData .

You should be careful to lock blocks only when necessary, because locked relocatable blocks can increase heap fragmentation and slow down your application unnecessarily. You should lock a handle only if you dereference it, directly or indirectly, and then use a copy of the original master pointer after calling a routine that could move or purge memory. When you no longer need to reference the block with the master pointer, you should unlock the handle. In Listing 1-1 , the handle myData is never explicitly unlocked. Instead, the original attributes of the handle are saved by calling HGetState and later are restored by calling HSetState . This strategy is preferable to just calling HLock and HUnlock .

A compiler can generate hidden dereferencing, and hence potential dangling pointers, in other ways, for instance, by assigning the result of a function that might move or purge blocks to a field in a record referenced by a handle. Such problems are particularly common in code that manipulates linked data structures. For example, you might use this code to allocate a new element of a linked list:

myHandle^^.nextHandle := NewHandle(sizeof(myLinkedElement));

This can cause problems because your compiler could dereference myHandle before calling NewHandle . Therefore, you should either lock myHandle before performing the allocation, or use a temporary variable to allocate the new handle, as in the following code:

tempHandle := NewHandle(sizeof(myLinkedElement));
myHandle^^.nextHandle := tempHandle;

Passing fields of records as arguments to routines that might move or purge memory can cause similar problems, if the records are in relocatable blocks referred to with handles. Problems arise only when you pass a field by reference rather than by value. Pascal conventions call for all arguments larger than 4 bytes to be passed by reference. In Pascal, a variable is also passed by reference when the routine called requests a variable parameter. Both of the following lines of code could leave a pointer dangling:

TEUpdate(hTE^^.viewRect, hTE);
InvalRect(theControl^^.contrlRect);

These problems occur because a compiler may dereference a handle before calling the routine to which you pass the handle. Then, that routine may move memory before it uses the dereferenced handle, which might then be invalid. As before, you can solve these problems by locking the handles or using temporary variables.

Loading Code Segments

If you call an application-defined routine located in a code segment that is not currently in RAM, the Segment Manager might need to move memory when loading that code segment, thus jeopardizing any dereferenced handles you might be using. For example, suppose you call an application-defined procedure ManipulateData , which manipulates some data at an address passed to it in a variable parameter.

PROCEDURE MyRoutine;
BEGIN
    ...
    ManipulateData(myHandle^);
    ...
END;

You can create a dangling pointer if ManipulateData and MyRoutine are in different segments, and the segment containing ManipulateData is not loaded when MyRoutine is executed. You can do this because you've passed a dereferenced copy of myHandle as an argument to ManipulateData . If the Segment Manager must allocate a new relocatable block for the segment containing ManipulateData , it might move myHandle to do so. If so, the dereferenced handle would dangle. A similar problem can occur if you assign the result of a function in a nonresident code segment to a field in a record referred to by a handle.

You need to be careful even when passing a field in a record referenced by a handle to a routine in the same code segment as the caller, or when assigning the result of a function in the same code segment to such a field. If that routine could call a Toolbox routine that might move or purge memory, or call a routine in a different, nonresident code segment, then you could indirectly cause a pointer to dangle.

Callback Routines

Code segmentation can also lead to a different type of dangling-pointer problem when you use callback routines. The problem rarely arises, but it is difficult to debug. Some Toolbox routines require that you pass a pointer to a procedure in a variable of type ProcPtr . Ordinarily, it does not matter whether the procedure you pass in such a variable is in the same code segment as the routine that calls it or in a different code segment. For example, suppose you call TrackControl as follows:

myPart := TrackControl(myControl, myEvent.where, @MyCallBack);

If MyCallBack were in the same code segment as this line of code, then a compiler would pass to TrackControl the absolute address of the MyCallBack procedure. If it were in a different code segment, then the compiler would take the address from the jump table entry for MyCallBack . Either way, TrackControl should call MyCallBack correctly.

Occasionally, you might use a variable of type ProcPtr to hold the address of a callback procedure and then pass that address to a routine. Here is an example:

myProc := @MyCallBack;
...
myPart := TrackControl(myControl, myEvent.where, myProc);

As long as these lines of code are in the same code segment and the segment is not unloaded between the execution of those lines, the preceding code should work perfectly. Suppose, however, that myProc is a global variable, and the first line of the code is in a different segment from the call to TrackControl . Suppose, further, that the MyCallBack procedure is in the same segment as the first line of the code (which is in a different segment from the call to TrackControl ). Then, the compiler might place the absolute address of the MyCallBack routine into the variable myProc . The compiler cannot realize that you plan to use the variable in a different code segment from the one that holds both the routine you are referencing and the routine you are using to initialize the myProc variable. Because MyCallBack and the call to TrackControl are in different code segments, the TrackControl procedure requires that you pass an address in the jump table, not an absolute address. Thus, in this hypothetical situation, myProc would reference MyCallBack incorrectly.

To avoid this problem, make sure to place in the same segment any code in which you assign a value to a variable of type ProcPtr and any code in which you use that variable. If you must put them in different code segments, then be sure that you place the callback routine in a code segment different from the one that initializes the variable.

Some development systems allow you to specify compiler options that force jump table references to be generated for routine addresses. If you specify those options, the problems described in this section cannot arise.


© 1997 Apple Computer, Inc.

Previous | Chapter Top | Chapter Contents | Next